package com.zxjbyte.yiyi.framework.common.core.okhttp;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.zxjbyte.yiyi.framework.common.constant.Constant;
import com.zxjbyte.yiyi.framework.common.constant.Method;
import com.zxjbyte.yiyi.framework.common.core.okhttp.callback.AbstractAsyncCallback;
import com.zxjbyte.yiyi.framework.common.core.okhttp.callback.IProgressCallback;
import com.zxjbyte.yiyi.framework.common.domain.FileItem;
import com.zxjbyte.yiyi.framework.common.domain.Pair;
import com.zxjbyte.yiyi.framework.common.exception.ServiceException;
import com.zxjbyte.yiyi.framework.common.util.ClassXUtils;
import com.zxjbyte.yiyi.framework.common.util.JsonUtil;
import com.zxjbyte.yiyi.framework.common.util.ServletXUtil;
import com.zxjbyte.yiyi.framework.common.util.ValidationUtil;
import lombok.SneakyThrows;
import okhttp3.*;
import okhttp3.internal.http.HttpMethod;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLConnection;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;

import static com.zxjbyte.yiyi.framework.common.constant.StringPool.*;

/**
 * okHttp 请求封装
 * @Author zhangxingjia
 * @Date 2023/4/20 11:12
 * @Version: 1.0
 */
public class OkRequest {
    private OkHttpClient client;
    private Request request;
    private String method;
    private String contentType;
    private Map<String, Object> formParams;
    private List<Pair> queryParams;
    private Object body;
    private final Map<String, String> headerParams = new HashMap<>(16);
    private final HttpUrl.Builder httpUrlBuilder;
    private final OkHttpCliHolder okHttpCliHolder;


    private OkRequest(String path){
        HttpUrl httpUrl = HttpUrl.parse(path);
        ValidationUtil.checkNotNull(httpUrl, "URL cannot be parsed：[{}]", path);
        httpUrlBuilder = httpUrl.newBuilder();
        this.addDefaultHeader("User-Agent", Constant.DEFAULT_USER_AGENT);
        okHttpCliHolder = OkHttpCliHolder.defaultCreate();
    }

    /**
     * Create JSON post mode request
     * @param path
     * @param body
     * @param headerParams
     * @return
     */
    public static OkRequest createPostJson(String path, Object body, Map<String, String> headerParams){
        return of(path).method(Method.POST).contentType("application/json;charset=UTF-8")
                .headers(headerParams).body(body);
    }

    /**
     * Create From post mode request
     * @param path
     * @param formParams
     * @param headerParams
     * @return
     */
    public static OkRequest createPostForm(String path, Map<String, Object> formParams, Map<String, String> headerParams){
        return of(path).method(Method.POST)
                .contentType("application/x-www-form-urlencoded").form(formParams).headers(headerParams);
    }

    /**
     * Create get request
     * @param path
     * @param queryParams
     * @param headerParams
     * @return
     */
    public static OkRequest createGet(String path, List<Pair> queryParams, Map<String, String> headerParams){
        return of(path).method(Method.GET).headers(headerParams).query(queryParams);
    }

    /**
     * Create get request
     * @param path
     * @return
     */
    public static OkRequest createGet(String path){
        return createGet(path, null,null);
    }

    /**
     * Create post upload
     * @param path
     * @param formParams
     * @return
     */
    public static OkRequest createUpload(String path, Map<String, Object> formParams){
        return of(path).method(Method.POST).contentType("multipart/form-data").form(formParams);
    }



    /**
     * Create get download
     * @param path
     * @param queryParams
     * @param headerParams
     * @return
     */
    public static OkRequest createDownload(String path, List<Pair> queryParams, Map<String, String> headerParams){
        return createGet(path, queryParams, headerParams);
    }

    /**
     * Create get download
     * @param path
     * @return
     */
    public static OkRequest createDownload(String path){
        return createDownload(path, null, null);
    }



    public static OkRequest of(String path){
        return new OkRequest(path);
    }

    /**
     * 同步执行
     * @return
     */
    @SneakyThrows
    public OkResponse execute(){
        if(this.client == null){
            this.client = okHttpCliHolder.build();
        }
        buildRequest(null);
        Call call = client.newCall(request);
        return OkResponse.of(call.execute());
    }

    /**
     * 异步执行
     * @param callback 异步回调基类
     */
    public void executeAsync(AbstractAsyncCallback callback){
        ValidationUtil.checkNotNull(callback, "Callback");
        if(this.client == null){
            client = okHttpCliHolder.build();
        }
        buildRequest(callback.getProgressCallback());
        Call call = client.newCall(request);
        call.enqueue(new Callback() {

            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                callback.onFailure(call, e);
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                callback.onResponse(call, response);
            }
        });
    }

    // --------------------------自定义请求属性 start---------------------------

    public OkRequest client(OkHttpClient client) {
        this.client = client;
        return this;
    }

    public OkRequest log(LoggerLevel level){
        okHttpCliHolder.log(level);
        return this;
    }

    public OkRequest header(String name, String value){
        headerParams.put(name, value);
        return this;
    }

    public OkRequest headers(Map<String, String> headerParams){
        if(CollUtil.isNotEmpty(headerParams)){
            this.headerParams.putAll(headerParams);
        }
        return this;
    }

    public OkRequest method(String method) {
        this.method = method;
        return this;
    }

    public OkRequest timeout(long milliseconds){
        okHttpCliHolder.readTimeout(Duration.ofMillis(milliseconds));
        okHttpCliHolder.connectTimeout(Duration.ofMillis(milliseconds));
        return this;
    }

    public OkRequest connectTimeout(long milliseconds){
        okHttpCliHolder.connectTimeout(Duration.ofMillis(milliseconds));
        return this;
    }

    public OkRequest readTimeout(long milliseconds){
        okHttpCliHolder.readTimeout(Duration.ofMillis(milliseconds));
        return this;
    }

    public OkRequest contentType(String contentType){
        this.contentType = contentType;
        return this;
    }

    public OkRequest form(String name, Object value){
        putToForm(name, value);
        return this;
    }

    public OkRequest form(Map<String, Object> formParams){
        if(CollUtil.isNotEmpty(formParams)){
            formParams.forEach(this::putToForm);
        }
        return this;
    }

    public OkRequest query(String key, String value){
        putToQuery(key, value);
        return this;
    }

    public OkRequest query(List<Pair> queryParams){
        if(CollUtil.isNotEmpty(queryParams)){
            queryParams.forEach(pair -> {
                if(StrUtil.isNotBlank(pair.getKey()) && StrUtil.isNotBlank(pair.getValue())){
                    this.queryParams.add(pair);
                }
            });
        }
        return this;
    }

    public OkRequest body(Object body){
        this.body = body;
        return this;
    }

    // --------------------------自定义请求属性 end---------------------------

    /**
     * Build an HTTP request with the given options.
     */
    @SneakyThrows
    private void buildRequest(IProgressCallback callback){
        RequestBody requestBody;
        if(!HttpMethod.permitsRequestBody(this.method)){
            requestBody = null;
        } else if("application/x-www-form-urlencoded".equals(this.contentType)){
            requestBody = buildRequestBodyForm(this.formParams);
        } else if("multipart/form-data".equals(this.contentType)){
            requestBody = buildRequestBodyMultipart(this.formParams);
        } else {
            requestBody = serialize(this.body);
        }
        buildRequestQueryParams(this.queryParams);
        final Request.Builder builder = new Request.Builder().url(httpUrlBuilder.build());
        processHeaderParams(builder);
        // tag对象与当前请求相关联, 主要用途是方便开发者在请求发送到服务器之后，还能够在后续的流程中追踪和处理这个请求，例如取消这个请求或者根据不同的tag来处理不同的响应等
        builder.tag(callback);
        if(callback != null && requestBody != null){
            this.request = builder.method(this.method, new CountingRequestBody(requestBody, callback)).build();
        }else {
            this.request = builder.method(this.method, requestBody).build();
        }
    }


    private void processHeaderParams(Request.Builder builder) {
        if(CollUtil.isNotEmpty(this.headerParams)){
            for (Map.Entry<String, String> param : this.headerParams.entrySet()) {
                builder.header(param.getKey(), param.getValue());
            }
        }
    }

    private RequestBody serialize(Object body){
        if(body instanceof byte[]) {
            return RequestBody.create((byte[]) body, MediaType.parse(this.contentType));
        } else if("text/plain".equals(this.contentType) && body instanceof String){
            return RequestBody.create((String) body, MediaType.parse(this.contentType));
        } else if(ServletXUtil.isJsonMime(this.contentType)){
            String jsonBody;
            if(ObjectUtil.isNull(body)){
                jsonBody = EMPTY;
            }else if (body instanceof String){
                jsonBody = (String) body;
            }else {
                jsonBody = JsonUtil.toJsonAsString(body);
            }
            return RequestBody.create(jsonBody, MediaType.parse(this.contentType));
        }else {
            throw new ServiceException("Content type: [" + this.contentType + "] is not supported");
        }
    }

    /**
     * encodes the query parameter using UTF-8 and adds it to this URL's query string
     * @param queryParams
     */
    private void buildRequestQueryParams(List<Pair> queryParams){
        if(CollUtil.isNotEmpty(queryParams)){
            queryParams.forEach(s -> this.httpUrlBuilder.addQueryParameter(s.getKey(), s.getValue()));
        }
    }

    private RequestBody buildRequestBodyMultipart(Map<String, Object> formParams){
        MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
        formParams.forEach((k, v) -> {
            if(v instanceof File){
                addPartToMultiPartBuilder(builder, k, (File)v);
            } else if(v instanceof FileItem){
                addFileItemToMultiPartBuilder(builder, k, (FileItem) v);
            } else if (v instanceof Collection){
                for(Object object : (Collection)v){
                    if(object instanceof File){
                        addPartToMultiPartBuilder(builder, k, (File)object);
                    } else if(object instanceof FileItem){
                        addFileItemToMultiPartBuilder(builder, k, (FileItem) object);
                    }
                }
            } else {
                Headers partHeaders = Headers.of("Content-Disposition", "form-data; name=\"" + k + "\"");
                builder.addPart(partHeaders, RequestBody.create(parameterToStr(v), null));
            }
        });
        return builder.build();
    }

    private RequestBody buildRequestBodyForm(Map<String, Object> formParams){
        okhttp3.FormBody.Builder formBuilder = new okhttp3.FormBody.Builder();
        formParams.forEach((k ,v) -> formBuilder.add(k, parameterToStr(v)));
        return formBuilder.build();
    }

    private void addPartToMultiPartBuilder(MultipartBody.Builder builder, String key, File file) {
        MediaType mediaType = MediaType.parse(guessContentTypeFromFile(file));
        builder.addFormDataPart(key, file.getName(), RequestBody.create(file, mediaType));
    }

    private void addFileItemToMultiPartBuilder(MultipartBody.Builder builder, String key, FileItem fileItem) {
        builder.addFormDataPart(key, fileItem.getFileName(), RequestBody.create(fileItem.getBytes(), MediaType.parse(fileItem.getMimeTypeEmptyToDefault("application/octet-stream;charset=utf-8"))));
    }

    private String guessContentTypeFromFile(File file) {
        String contentType = URLConnection.guessContentTypeFromName(file.getName());
        if (contentType == null) {
            return "application/octet-stream;charset=utf-8";
        } else {
            return contentType;
        }
    }

    private String parameterToStr(Object param){
        if(param == null){
            return EMPTY;
        }else if(param instanceof Date
                || param instanceof LocalDateTime
                || param instanceof LocalDate
                || param instanceof LocalTime){
            String jsonStr = JsonUtil.toJsonAsString(param);
            return StrUtil.sub(jsonStr, 1, jsonStr.length() - 1);
        }else if(param instanceof Collection){
            return CollUtil.join((Collection) param, COMMA);
        }else if(param instanceof String){
            return (String) param;
        }else{
            return JsonUtil.toJsonAsString(param);
        }
    }

    /**
     * Add a default header.
     *
     * @param key The header's key
     * @param value The header's value
     */
    private void addDefaultHeader(String key, String value) {
        headerParams.put(key, value);
    }

    private void putToForm(String name, Object value) {
        if (null == name || null == value) {
            return;
        }
        if (null == this.formParams) {
            this.formParams = new HashMap<>(16);
        }
        this.formParams.put(name, value);
    }

    private void putToQuery(String key, String value) {
        if (null == key || null == value) {
            return;
        }
        if (null == this.queryParams) {
            this.queryParams = new ArrayList<>(16);
        }
        this.queryParams.add(Pair.of(key, value));
    }

}
